/**
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 25/05/2012
 *
 */
package br.com.vfcfw.controller.base;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;

import javax.annotation.PostConstruct;
import javax.faces.component.UIForm;
import javax.faces.component.UIViewRoot;
import javax.faces.model.DataModel;

import org.apache.log4j.Logger;
import org.hibernate.Criteria;

import br.com.vfcfw.business.service.BaseService;
import br.com.vfcfw.business.service.IBaseService;
import br.com.vfcfw.commons.domain.entity.BaseEntity;
import br.com.vfcfw.commons.domain.filter.BaseFilter;
import br.com.vfcfw.commons.exception.ApplicationException;
import br.com.vfcfw.commons.exception.BusinessException;
import br.com.vfcfw.commons.util.ConstantesArquitetura;
import br.com.vfcfw.commons.util.GenericsUtil;
import br.com.vfcfw.commons.util.LogUtil;
import br.com.vfcfw.commons.util.MensagemUtil;
import br.com.vfcfw.commons.util.ReflexaoUtil;
import br.com.vfcfw.commons.util.ValidacaoUtil;
import br.com.vfcfw.commons.util.WeldUtil;
import br.com.vfcfw.controller.enums.OrdemClassificacao;
import br.com.vfcfw.controller.util.JSFUtil;

/**
 * Abstração com a implementação padrão dos ManegedBeans (<tt>Faces</tt>).
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 25/05/2012
 * 
 * @param <E> {@link BaseEntity} Entidade mapeada com JPA, principal do Managed Bean.
 * @param <F> {@link BaseFilter} Filtro da entidade mapeada com JPA, principal do Managed Bean.
 * @param <B> {@link BaseEntity} ou subclasse business da entidade mapeada com JPA, principal do Managed, usada para a
 *        GRID.
 */
public abstract class BaseFaces<E extends BaseEntity, F extends BaseFilter<E>, B extends E> implements Serializable {

    private static final long serialVersionUID = -4857228579142663250L;

    /** String para direcionar para a tela de login da aplicação. */
    protected static final String FWD_LOGIN = "login";

    /** String para manter na mesma página. */
    protected static final String FWD_NAO_NAVEGAR = "";

    /** Nome da imagem para a situação de "não ordenado". */
    public static final String IMG_PAGINACAO_NAO_ORDENADO = "nao-ordenado.gif";

    /** Nome da imagem para ordenação ascedente. */
    public static final String IMG_PAGINACAO_ASCEDENTE = "crescente.gif";

    /** Nome da imagem para ordenação descedente. */
    public static final String IMG_PAGINACAO_DESCEDENTE = "decrescente.gif";

    /** Logger para registrar eventos do Managed Bean. */
    protected Logger logger = Logger.getLogger(getClass());

    private TimeZone timezone = TimeZone.getDefault();

    /**
     * Variável genérica, referenciando a entidade, mapeada com JPA, principal do Managed Bean.
     */
    protected E entity;

    /**
     * Variável genérica, referenciando a entidade, mapeada com JPA, principal do Managed Bean selecionado para
     * edição/exclusão.
     */
    protected E entitySelecionado;

    /**
     * Variável genérica, referenciando o filtro de pesquisa da entidade, mapeada com JPA, principal do Managed Bean.
     */
    protected F filtroPesquisa;

    /**
     * Varriável genérica, referenciando a entidade (ou subclasse business), mapeada com JPA, principal do Managed Bean.
     */
    protected B entityBusiness;

    /**
     * Lista do tipo da entidade, mapeada com JPA, principal do Managed Bean, usada para receber o resultado de uma
     * pesquisa.
     */
    protected List<E> listResultadoPesquisa;

    /**
     * {@link BaseService} Classe de negócio da entidade principal do Managed Bean.
     */
    protected IBaseService<E> entityService;

    /**
     * Classe da entidade, mapeada com JPA, principal do Managed Bean.
     */
    @SuppressWarnings("unchecked")
    private Class<E> entityClass = GenericsUtil.getSuperClassGenericType(getClass(), 0);

    /**
     * Classe do filtro da entidade, mapeada com JPA, principal do Managed Bean.
     */
    @SuppressWarnings("unchecked")
    private Class<F> filterClass = GenericsUtil.getSuperClassGenericType(getClass(), 1);

    /**
     * Classe da entidade, mapeada com JPA, principal do Managed Bean, usada no GRID.
     */
    @SuppressWarnings("unchecked")
    private Class<B> entityBusinessClass = GenericsUtil.getSuperClassGenericType(getClass(), 2);

    /**
     * {@link UIForm} Componente que deverá receber o binding do formulário da tela associada ao Managed Bean.
     */
    private UIForm form;

    /**
     * Variável booleana para determinar se o painel do filtro de pesquisa será exibido.
     */
    private boolean exibirFiltro;

    /**
     * Variável booleana para verificar se houve alteração de algum registro.
     */
    private boolean registroAlterado;

    /**
     * Mapa com os campos de ordenação.
     */
    private Map<String, String> camposOrdenacao = new HashMap<String, String>(1);

    /**
     * Campo de ordenação.
     */
    private String campoOrdenacao;

    /**
     * Flag que indica que o Managed bean usará paginação por demanda.
     */
    private boolean paginacaoPorDemanda = true;

    /**
     * Flag que indica que a pesquisa será por criteria.
     */
    private boolean pesquisaPorCriteria = true;

    /**
     * Construtor da classe.
     */
    public BaseFaces() {

        super();
    }

    /**
     * Método a ser implementado e anotado com <b>@PostConstruct</b> para inicializar os objetos usados na tela com
     * informações carregadas do banco.
     */
    @PostConstruct
    public abstract void inicializar();

    /**
     * Verifica se a página está sendo executada no postback (<tt>página reenviando dados para ela mesma</tt>).
     * 
     * @return <tt>TRUE</tt> se estiver sendo executada no postback, <tt>FALSE</tt> caso contrário.
     */
    protected boolean isPostback() {

        return JSFUtil.getFacesContext().isPostback();
    }

    /**
     * Retorna a entidade mapeada com JPA.
     * 
     * @return Entidade mapeada com JPA.
     */
    public E getEntity() {

        return entity;
    }

    /**
     * Especifica a entidade mapeada com JPA.
     * 
     * @param entity Entidade mapeada com JPA.
     */
    public void setEntity(E entity) {

        this.entity = entity;
    }

    /**
     * Retorna o valor do atributo entitySelecionado.
     * 
     * @return Valor do atributo entitySelecionado.
     */
    public E getEntitySelecionado() {

        return entitySelecionado;
    }

    /**
     * Especifica o valor do atributo entitySelecionado.
     * 
     * @param entitySelecionado E do entitySelecionado a ser setado.
     */
    public void setEntitySelecionado(E entitySelecionado) {

        this.entitySelecionado = entitySelecionado;
    }

    /**
     * Retorna o valor do atributo entityClass.
     * 
     * @return Valor do atributo entityClass.
     */
    public Class<E> getEntityClass() {

        return entityClass;
    }

    /**
     * Retorna o valor do atributo filterClass.
     * 
     * @return Valor do atributo filterClass.
     */
    public Class<F> getFilterClass() {

        return filterClass;
    }

    /**
     * Retorna o valor do atributo entityService.
     * 
     * @return Valor do atributo entityService.
     */
    public IBaseService<E> getEntityService() {

        return entityService;
    }

    /**
     * Retorna o valor do atributo entityBusinessClass.
     * 
     * @return Valor do atributo entityBusinessClass.
     */
    public Class<B> getEntityBusinessClass() {

        return entityBusinessClass;
    }

    /**
     * Especifica o valor do atributo entityService.
     * 
     * @param entityService BaseService<E> do entityService a ser setado.
     */
    public void setEntityService(IBaseService<E> entityService) {

        this.entityService = entityService;
    }

    /**
     * Retorna o filtro pesquisa.
     * 
     * @return Valor do atributo filtroPesquisa.
     */
    public F getFiltroPesquisa() {

        return filtroPesquisa;
    }

    /**
     * Especifica o filtro pesquisa.
     * 
     * @param filtroPesquisa Filtro de pesquisa da entidade mapeada com JPA.
     */
    public void setFiltroPesquisa(F filtroPesquisa) {

        this.filtroPesquisa = filtroPesquisa;
    }

    /**
     * Retorna a lista do tipo da entidade mapeada com JPA, usada para receber o resultado de uma pesquisa.
     * 
     * @return Lista do tipo da entidade mapeada com JPA.
     */
    public List<E> getListResultadoPesquisa() {

        return listResultadoPesquisa;
    }

    /**
     * Especifica a lista do tipo da entidade mapeada com JPA, usada para receber o resultado de uma pesquisa.
     * 
     * @param listResultadoPesquisa Lista do tipo da entidade mapeada com JPA.
     */
    public void setListResultadoPesquisa(List<E> listResultadoPesquisa) {

        this.listResultadoPesquisa = listResultadoPesquisa;
    }

    /**
     * Verifica se o painel do filtro de pesquisa será exibido.
     * 
     * @return <tt>TRUE</tt> o painel do filtro de pesquisa será exibido, <tt>FALSE</tt> caso contrário.
     */
    public boolean isExibirFiltro() {

        return exibirFiltro;
    }

    /**
     * Especifica se o painel do filtro de pesquisa será exibido.
     * 
     * @param exibirFiltro <tt>TRUE</tt> o painel do filtro de pesquisa será exibido, <tt>FALSE</tt> caso contrário.
     */
    public void setExibirFiltro(boolean exibirFiltro) {

        this.exibirFiltro = exibirFiltro;
    }

    /**
     * Verifica se houve alteração em algum registro.
     * 
     * @return <tt>TRUE</tt> houve alteração, <tt>FALSE</tt> caso contrário.
     */
    public boolean isRegistroAlterado() {

        return registroAlterado;
    }

    /**
     * Especifica se houve alteração em algum registro.
     * 
     * @param registroAlterado <tt>TRUE</tt> houve alteração, <tt>FALSE</tt> caso contrário.
     */
    public void setRegistroAlterado(boolean registroAlterado) {

        this.registroAlterado = registroAlterado;
    }

    /**
     * Retorna o valor do atributo propriedadeOrdenacao.
     * 
     * @return Valor do atributo propriedadeOrdenacao.
     */
    public String getPropriedadeOrdenacao() {

        return campoOrdenacao;
    }

    /**
     * Especifica o valor do atributo propriedadeOrdenacao.
     * 
     * @param propriedadeOrdenacao String do propriedadeOrdenacao a ser setado.
     */
    public void setPropriedadeOrdenacao(String propriedadeOrdenacao) {

        this.campoOrdenacao = propriedadeOrdenacao;
    }

    /**
     * Retorna o valor do atributo ordensClassificacao.
     * 
     * @return Valor do atributo ordensClassificacao.
     */
    public Map<String, String> getCamposOrdenacao() {

        return camposOrdenacao;
    }

    /**
     * Especifica o valor do atributo ordensClassificacao.
     * 
     * @param ordensClassificacao Map<String,SortOrder> do ordensClassificacao a ser setado.
     */
    public void setCamposOrdenacao(Map<String, String> ordensClassificacao) {

        this.camposOrdenacao = ordensClassificacao;
    }

    /**
     * Retorna o valor do atributo campoOrdenacao.
     * 
     * @return Valor do atributo campoOrdenacao.
     */
    public String getCampoOrdenacao() {

        return campoOrdenacao;
    }

    /**
     * Especifica o valor do atributo campoOrdenacao.
     * 
     * @param campoOrdenacao String do campoOrdenacao a ser setado.
     */
    public void setCampoOrdenacao(String campoOrdenacao) {

        this.campoOrdenacao = campoOrdenacao;
    }

    /**
     * Retorna o valor do atributo form.
     * 
     * @return Valor do atributo form.
     */
    public UIForm getForm() {

        return form;
    }

    /**
     * Especifica o valor do atributo form.
     * 
     * @param form UIForm do form a ser setado.
     */
    public void setForm(UIForm form) {

        this.form = form;
    }

    /**
     * Retorna o {@link DataModel} que será usado para a paginação sobre demanda.
     * 
     * @return {@link DataModel}.
     */
    public abstract DataModel<B> getDataModel();

    /**
     * Retorna o {@link DataModel} que será usado para a paginação sobre demanda.
     * 
     * @param dataModel Implementação do {@link DataModel}.
     * @return {@link DataModel}.
     */
    protected DataModel<B> getDataModel(DataModel<B> dataModel) {

        return dataModel;
    }

    /**
     * Inverte a ordenação.
     */
    public void inverterOrdenacao(String campoOrdenacao) {

        this.campoOrdenacao = campoOrdenacao;

        for (Entry<String, String> entry : camposOrdenacao.entrySet()) {

            OrdemClassificacao novaOrdenacao = null;

            if (entry.getKey().equals(campoOrdenacao)) {

                if (entry.getValue().equals(OrdemClassificacao.ascending.name())) {
                    novaOrdenacao = OrdemClassificacao.descending;
                } else {
                    novaOrdenacao = OrdemClassificacao.ascending;
                }
            } else {
                novaOrdenacao = OrdemClassificacao.unsorted;
            }

            entry.setValue(novaOrdenacao.name());
        }
    }

    /**
     * Retorna o nome da imagem de ordenação.
     * 
     * @return Nome da imagem.
     */
    public String getImagemOrdenacao() {

        String nomeImagem = IMG_PAGINACAO_NAO_ORDENADO;

        Object sortOrder = camposOrdenacao.get(campoOrdenacao);

        if (sortOrder == null) {

            nomeImagem = IMG_PAGINACAO_NAO_ORDENADO;
        } else {

            if (OrdemClassificacao.ascending.equals(sortOrder)) {

                nomeImagem = IMG_PAGINACAO_ASCEDENTE;
            } else {

                nomeImagem = IMG_PAGINACAO_DESCEDENTE;
            }
        }

        return nomeImagem;
    }

    /**
     * Verifica se o Managed Bean usará paginação por demanda.
     * 
     * @return <tt>TRUE</tt> usa paginação por demanda, <tt>FALSE</tt> não usa.
     */
    public boolean isPaginacaoPorDemanda() {

        return paginacaoPorDemanda;
    }

    /**
     * Especifica se o Managed Bean usará paginação por demanda.
     * 
     * @param paginacaoPorDemanda <tt>TRUE</tt> para usar paginação por demanda, <tt>FALSE</tt> caso contrário.
     */
    public void setPaginacaoPorDemanda(boolean paginacaoPorDemanda) {

        this.paginacaoPorDemanda = paginacaoPorDemanda;
    }

    /**
     * Verifica se a paginação usará filtro por {@link Criteria}.
     * 
     * @return <tt>TRUE</tt> usará o filtro por {@link Criteria}, <tt>FALSE</tt> usará o filtro por HQL.
     */
    public boolean isPesquisaPorCriteria() {

        return pesquisaPorCriteria;
    }

    /**
     * Especifica que a paginação usará o filtro por {@link Criteria}.
     * 
     * @param pesquisaPorCriteria <tt>TRUE</tt> usará o filtro por {@link Criteria}, <tt>FALSE</tt> usará o filtro por
     *        HQL.
     */
    public void setPesquisaPorCriteria(boolean pesquisaPorCriteria) {

        this.pesquisaPorCriteria = pesquisaPorCriteria;
    }

    /**
     * Redireciona para uma outra página passando um parâmetro.
     * 
     * @param nomePagina Nome da página.
     * @param nomeParametro Nome do parâmetro.
     * @param valorParametro Valor do parâmetro.
     * @return String de redirecionamento.
     */
    protected String redirecionarComParametro(String nomePagina, String nomeParametro, Long valorParametro) {

        return redirecionarComParametro(nomePagina, new String[] { nomeParametro }, new Object[] { valorParametro });
    }

    /**
     * Redireciona para uma outra página passando parâmetros.
     * 
     * @param nomePagina Nome da página.
     * @param nomesParametros Array com os nomes dos parâmetros.
     * @param valoresParametros Array com os valroes dos parâmetros.
     * @return String de redirecionamento.
     */
    protected String redirecionarComParametro(String nomePagina, String[] nomesParametros, Object[] valoresParametros) {

        StringBuilder sbRedirect = new StringBuilder(nomePagina);

        sbRedirect.append("?faces-redirect=true&amp;");

        for (int i = 0; i < nomesParametros.length; i++) {

            sbRedirect.append(nomesParametros[i]).append("=").append(valoresParametros[i]);

            if (i + 1 < nomesParametros.length) {

                sbRedirect.append("&amp;");
            }
        }
        return sbRedirect.toString();
    }

    /**
     * Adiciona uma propriedade que pode ser usada na ordenação do datatable.
     * 
     * @param propriedadeOrdenacao Atributo da entidade persistente.
     */
    protected void adicionarCampoOrdenacao(String propriedadeOrdenacao) {

        this.getCamposOrdenacao().put(propriedadeOrdenacao, OrdemClassificacao.unsorted.name());
    }

    /**
     * Exibe uma mensagem de erro para o usuário.
     * 
     * @param mensagem {@link String} Mensagem de erro.
     */
    protected void exibirMensagemErro(String mensagem) {

        exibirMensagemErro(null, mensagem);
    }

    /**
     * Exibe uma mensagem de erro para o usuário.
     * 
     * @param idComponente Identificador do componente (<tt>idForm:idComponente</tt>).
     * @param mensagem {@link String} Mensagem de erro.
     */
    protected void exibirMensagemErro(String idComponente, String mensagem) {

        exibirMensagemErro(idComponente, mensagem, new BusinessException(mensagem));
    }

    /**
     * Exibe uma mensagem de erro para o usuário.
     * 
     * @param mensagem Mensagem de erro.
     * @param exception {@link Exception}.
     */
    protected void exibirMensagemErro(String mensagem, Exception exception) {

        exibirMensagemErro(null, mensagem, exception);
    }

    /**
     * Exibe uma mensagem de erro para o usuário.
     * 
     * @param idComponente Identificador do componente (<tt>idForm:idComponente</tt>).
     * @param mensagem Mensagem de erro.
     * @param exception {@link Exception}.
     */
    protected void exibirMensagemErro(String idComponente, String mensagem, Exception exception) {

        JSFUtil.adicionarMensagemErro(idComponente, mensagem);
        JSFUtil.getFacesContext().validationFailed();
        LogUtil.error(logger, exception, mensagem);
    }

    /**
     * Exibe as mensagens de erro para o usuário.
     * 
     * @param msgs {@link List} Lista de mensagens.
     */
    protected void exibirMensagemErro(BusinessException exception) {

        StringBuilder mensagensErro = new StringBuilder();

        for (String msgErro : exception.getListaErros()) {

            mensagensErro.append(msgErro).append("\n");
        }
        mensagensErro.delete(mensagensErro.length() - 2, mensagensErro.length());

        exibirMensagemErro(mensagensErro.toString(), exception);
    }

    /**
     * Exibe uma mensagem de erro interno do sistema para o usuário.
     * 
     * @param exception {@link Exception}.
     */
    protected void exibirMensagemErroInterno(Exception exception) {

        StringBuilder msgErro = new StringBuilder();
        msgErro.append(getMensagem(ConstantesArquitetura.MSG_ERRO_INTERNO_SISTEMA));
        msgErro.append(" ");
        msgErro.append(exception.getLocalizedMessage());

        exibirMensagemErro(msgErro.toString(), exception);
    }

    /**
     * Exibe uma mensagem de informação para o usuário.
     * 
     * @param msg Mensagem.
     */
    protected void exibirMensagemInformacao(String msg) {

        JSFUtil.adicionarMensagemInformacao(msg);
    }

    /**
     * Retorna o texto, no arquivo de recurso, correspondente a chave passada para localidade definida na
     * {@link UIViewRoot}.
     * 
     * @param chave Chave do texto no arquivo de recursos.
     * @param argumentos Argumentos que substituirão os curingas no texto.
     * @return Texto.
     */
    protected String getMensagem(String chave, Object... argumentos) {

        return MensagemUtil.obterMensagem(JSFUtil.getLocale(), chave, argumentos);
    }

    /**
     * Returns a managed bean instantiated by name
     * 
     * @param nomeManagedBean Nome do Managed bean.
     * @return Managed bean.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected <T extends BaseFaces> T getFaces(String nomeManagedBean) {

        T managedBean = (T) JSFUtil.getBean(nomeManagedBean);

        if (managedBean == null) {

            LogUtil.error(logger, String.format(
                "Managed bean com o nome %s não foi encontrado. Verifique se o nome informado esta correto.",
                nomeManagedBean));
        }
        return managedBean;
    }

    /**
     * Retorna a instância do bean especificado pelo tipo via CDI.
     * 
     * @param beanType Tipo do bean.
     * @return Instância do bean.
     */
    protected <T> T getBean(Class<T> beanType) {

        return WeldUtil.obterBean(beanType);
    }

    /**
     * Obtém uma nova instância da entidade, mapeada com JPA, principal do Managed Bean.
     * 
     * @return Nova instância.
     */
    protected E obterNovaInstanciaEntity() {

        E entityNovo = null;

        try {

            entityNovo = ReflexaoUtil.obterInstancia(entityClass);
        } catch (Exception e) {
            throw new BusinessException(getMensagem(ConstantesArquitetura.MSG_ERRO_REFLEXAO_INSTANCIAR_OBJETO,
                entityClass.getSimpleName()));
        }
        return entityNovo;
    }

    /**
     * Obtém uma nova instância do filter da entidade, mapeada com JPA, principal do Managed Bean.
     * 
     * @return Nova instância.
     */
    protected F obterNovaInstanciaFilter() {

        F filterNovo = null;

        try {

            filterNovo = ReflexaoUtil.obterInstancia(filterClass, entityClass, obterNovaInstanciaEntity());

        } catch (Exception e) {
            throw new BusinessException(getMensagem(ConstantesArquitetura.MSG_ERRO_REFLEXAO_INSTANCIAR_OBJETO,
                entityClass.getSimpleName()));
        }
        return filterNovo;
    }

    /**
     * Trata as exeptions exibindo mensagens para o usuário.
     * 
     * @param exception {@link Exception}.
     */
    protected void tratarException(Exception exception) {

        if (exception instanceof BusinessException) {

            BusinessException businessException = (BusinessException) exception;

            exibirMensagemErro(businessException);

        } else if (exception instanceof ApplicationException) {

            exibirMensagemErro(exception.getMessage(), exception);

        } else {

            exibirMensagemErroInterno(exception);
        }
    }

    // /**
    // * Retorna o usuário autenticado na aplicação.
    // *
    // * @return Usuário autenticado.
    // */
    // public static Usuario getUsuarioAutenticado() {
    //
    // return SegurancaUtil.getUsuarioAutenticado();
    // }

    /**
     * Verifica se o valor não é nulo ou vazio.
     * 
     * @param valor Valor.
     * @return <tt>TRUE</tt> se não for <b>nulo</b> ou <b>vazio</b>, <tt>FALSE</tt> caso contrário.
     */
    protected boolean possuiValor(Object valor) {

        return ValidacaoUtil.possuiValor(valor);
    }

    /**
     * Retorna o valor do atributo timezone.
     * 
     * @return Valor do atributo timezone.
     */
    public TimeZone getTimezone() {

        return timezone;
    }

}
